<template>
  <div class="cut-page-container">
    <h1>大文件上传</h1>
    <input type="file" @change="handleFileChange" />
    <el-button @click="handleUpload">上传</el-button>
    <el-button @click="handlePause" v-if="!isPaused">暂停</el-button>
    <el-button @click="handleResume" v-else>恢复</el-button>
    <div v-show="hashPercentage > 0">
      <h3>计算文件的hash</h3>
      <el-progress :percentage="hashPercentage"></el-progress>
      {{ "计算完成，文件hash为：" + hash }}
    </div>
    <div v-show="uploadPercentage > 0">
      <h3>大文件上传总进度</h3>
      <el-progress :percentage="uploadPercentage"></el-progress>
    </div>
    <div v-show="chunkList.length > 0">
      <h3>分片上传进度</h3>
      <el-table :data="chunkList" style="width: 100%">
        <el-table-column prop="chunkHash" label="分块" width="270">
        </el-table-column>
        <el-table-column prop="size" label="size(Kb)" width="90"
          :formatter="(row, column, value) => Math.floor(value / 1024)">
        </el-table-column>
        <el-table-column prop="percentage" label="上传进度">
          <template #default="scope">
            <el-progress :percentage="scope.row.percentage"></el-progress>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>


<script setup>
import { ref, computed, watch } from 'vue';
import SparkMD5 from 'spark-md5';

import { uploadVerify, uploadCut, uploadMerge } from '@/api/upload';
import { toast } from '@/utils/common';

// 定义切片的大小
const SIZE = 3 * 1024 * 1024;
// 上传状态
const Status = { wait: 1, error: 2, done: 3, fail: 4 };
// 文件对象
const File = ref(null);
// 切片列表
const chunkList = ref([]);
// 请求列表
const requestList = ref([]);
// 文件hash
const hash = ref("");
// 暂停状态
const isPaused = ref(false);
const hashPercentage = ref(0);
// 请求控制器
const controller = ref(null);

// 重置数据
const resetData = () => {
  chunkList.value = [];
  requestList.value = [];
  hash.value = null;
  hash.value = "";
  hashPercentage.value = 0;
};

// 判断文件是否可以秒传
const verifyUpload = async (fileName, fileHash) => {
  const { data } = await uploadVerify({ fileName, fileHash });
  return data;
}

// 计算总文件上传速度
const uploadPercentage = computed(
  () => {
    if (!File.value || !chunkList.value.length) return 0;
    const loaded = chunkList.value
      .map(item => item.size * item.percentage)
      .reduce((prev, next) => prev + next);

    return parseInt((loaded / File.value.size).toFixed(2));
  }
);

// 文件分片
const createFileChunk = (file, size = SIZE) => {
  const fileChunkList = [];
  let cur = 0;
  while (cur < file.size) {
    fileChunkList.push({ file: file.slice(cur, cur + size) });
    cur += size;
  }
  return fileChunkList;
}

// 计算hash
const calculateHash = (fileChunkList) => {
  return new Promise(
    (resolve, reject) => {
      const spark = new SparkMD5.ArrayBuffer();
      const reader = new FileReader();
      const file = File.value;
      const size = file.size;
      let offset = 2 * 1024 * 1024;
      let chunks = [file.slice(0, offset)];

      let cur = offset;
      while (cur < size) {
        // 最后一块全部加进来
        if (cur + offset >= size) {
          chunks.push(file.slice(cur, cur + offset));
        } else {
          // 中间的 前中后去两个字节
          const mid = cur + offset / 2;
          const end = cur + offset;
          chunks.push(file.slice(cur, cur + 2));
          chunks.push(file.slice(mid, mid + 2));
          chunks.push(file.slice(end - 2, end));
        }
        // 前取两个字节
        cur += offset;
      }

      reader.readAsArrayBuffer(new Blob(chunks));
      reader.onload = (e) => {
        spark.append(e.target.result);
        hashPercentage.value = 100;
        resolve(spark.end());
      };
    }
  );
}

// 上传进度监听函数
const onProgress = item => {
  return e => {
    item.percentage = parseInt(String((e.loaded / e.total) * 100));
  };
}

// 控制请求发送以及上传错误处理
const sendRequest = (requestList, max = 4) => {
  return new Promise(
    (resolve, reject) => {
      const len = requestList.length;
      // 发送成功的请求数
      let counter = 0;
      const retryArr = [];

      requestList.forEach((item) => (item.status = Status.wait));

      const start = async () => {
        let Err = false;
        while (counter < len && !isPaused.value && !Err) {
          // 创建请求列表
          const requestArr = [];

          // 并发控制请求
          for (let i = 0; i < max; i++) {
            let idx = requestList.findIndex(
              (item) => item.status == Status.wait || item.status == Status.error
            );
            if (idx == -1) {
              Err = true;
              return;
            }
            requestList[idx].status = Status.done;

            let { index } = requestList[idx];

            requestArr.push(
              uploadCut(
                requestList[idx],
                onProgress(chunkList.value[index]),
                controller.value.signal
              )
                .then(() => {
                  requestList[idx].status = Status.done;
                  counter++;
                  if (counter === len) {
                    resolve();
                  }
                })
                .catch((err) => {
                  requestList[idx].status = Status.error;
                  if (typeof retryArr[index] !== "number") {
                    if (!isPaused.value) {
                      toast(`第 ${index} 个片段上传失败，系统准备重试`, 'info');
                      retryArr[index] = 0;
                    }
                  }

                  // 次数累加
                  retryArr[index]++;
                  if (retryArr[index] > 3) {
                    toast(
                      `第 ${index} 个片段重试多次无效，系统准备放弃上传`, 'error'
                    );
                    requestList[idx].status = Status.fail;
                    requestList[idx].percentage = 0;
                    // 终止当前所有请求
                    Err = true;
                    requestArr.forEach(element => {
                      controller.value.abort();
                    });
                    requestArr = [];
                  }
                })
            );

            await Promise.all(requestArr);
          }
        }
      }

      start();
    }
  )
}

// 上传文件切片
const UploadChunks = async (uploadedList = []) => {
  if (controller.value) {
    controller.value = null;
  }

  controller.value = new AbortController();

  requestList.value = chunkList.value.filter((chunk) => {
    return !uploadedList.includes(chunk.chunkHash);
  });

  await sendRequest(requestList.value);
}

// 获取文件
const handleFileChange = e => {
  resetData();
  const [file] = e.target.files;
  if (!file) {
    File.value = null;
    return;
  }
  File.value = file;
}

// 上传文件
const handleUpload = async () => {
  const file = File.value;
  if (!file) {
    toast('请选择一个文件吧', 'warning');
    return;
  }

  resetData();

  // 文件分片
  const fileChunkList = createFileChunk(file);
  hash.value = await calculateHash(fileChunkList);

  const { isUpload, uploadList } = await verifyUpload(
    file.name,
    hash.value
  );

  if (!isUpload) {
    toast('秒传：上传成功');
    return;
  }

  chunkList.value = fileChunkList.map(({ file }, index) => {
    return {
      chunk: file,
      size: file.size,
      chunkHash: `${hash.value} - ${index}`,
      fileHash: hash.value,
      index,
      percentage: uploadList.includes(`${hash.value} - ${index}`) ? 100 : 0,
    };
  });

  UploadChunks(chunkList.value);
}

// 暂停上传
const handlePause = () => {
  requestList.value.forEach((element) => {
    controller.value.abort();
  });
  requestList.value = [];
  isPaused.value = true;
}

// 恢复上传
const handleResume = async () => {
  isPaused.value = false;
  const { uploadList } = await uploadVerify(File.value.name, hash.value);
  UploadChunks(uploadList);
}





// 监听上传进度来判断是否合并
watch(
  uploadPercentage,
  async val => {
    // 切片上传完成合并切片
    if (val == 100) {
      // 通知服务器合并切片
      const { success, msg } = await uploadMerge({
        fileName: File.value.name,
        fileSize: File.value.size,
        size: SIZE,
        hash: hash.value
      });

      if (success) {
        toast(msg);
      }
    }
  }
);
</script>


<style lang="less" scoped>
.cut-page-container {
  width: 100%;
  height: 100%;
  @apply p-10;
}
</style>